#pragma once

extern "C"
{
#include <cAnsInterface.h>
#include <cAnsQuery.h>
};
#pragma comment(lib,"ansys.lib")

#include <string>
#include <sstream>
#include <vector>
#include <array>
#include <tuple>
#include <map>
#include <algorithm>
#include <cstdarg>
#include <cstdio>
#include <cassert>
#include <atlconv.h>
#include <Shlwapi.h>

const char ENDLINE = '\n';
#define BUFFER_SIZE 65536

class CPathHelper
{
public:
	static std::string PathCombine(const std::string& path1, const std::string& path2)
	{
		char pathDest[MAX_PATH];
		::PathCombineA(pathDest, path1.c_str(), path2.c_str());
		return std::move(std::string{ pathDest });
	}

	static std::string PathCombine(const std::string& path1, const std::string& path2, const std::string& path3)
	{
		return PathCombine(PathCombine(path1, path2), path3);
	}

	static std::string PathNormalize(const std::string& path)
	{
		char pathDest[MAX_PATH];
		::PathCanonicalizeA(pathDest, path.c_str());
		return std::move(std::string{ pathDest });
	}
};

class CStringHelper
{
public:
	static std::vector<std::string> SplitString(const std::string& str, char delimiter)
	{
		std::vector<std::string> elems;
		std::stringstream ss(str);
		std::string item;
		while (std::getline(ss, item, delimiter))
		{
			elems.push_back(item);
		}
		return elems;
	}

	static std::vector<std::wstring> SplitString(const std::wstring& str, WCHAR delimiter)
	{
		std::vector<std::wstring> elems;
		std::wstringstream ss(str);
		std::wstring item;
		while (std::getline(ss, item, delimiter))
		{
			elems.push_back(item);
		}
		return elems;
	}

	static std::string ToLower(const std::string& str)
	{
		std::string str2{ str };
		std::transform(str.cbegin(), str.cend(), str2.begin(),
			[](char c) { return (('A' <= c) && (c <= 'Z')) ? (c - ('Z' - 'z')) : c; }
		);
		return str2;
	}

	static std::string ToUpper(std::string str)
	{
		std::string str2{ str };
		std::transform(str.cbegin(), str.cend(), str2.begin(),
			[](char c) { return (('a' <= c) && (c <= 'z')) ? (c + ('Z' - 'z')) : c; }
		);
		return str2;
	}

	static std::string Trim(const std::string& str, const std::string& whitespace = std::string{ " \t" })
	{
		const auto strBegin = str.find_first_not_of(whitespace);
		if (strBegin == std::string::npos)
			return ""; // no content

		const auto strEnd = str.find_last_not_of(whitespace);
		const auto strRange = strEnd - strBegin + 1;
		return str.substr(strBegin, strRange);
	}

	static std::wstring Trim(const std::wstring& str, const std::wstring& whitespace = std::wstring{ L" \t" })
	{
		const auto strBegin = str.find_first_not_of(whitespace);
		if (strBegin == std::string::npos)
			return L""; // no content

		const auto strEnd = str.find_last_not_of(whitespace);
		const auto strRange = strEnd - strBegin + 1;
		return str.substr(strBegin, strRange);
	}

	static std::wstring StringToWstring(const char* str)
	{
		USES_CONVERSION;
		return std::wstring{ CA2W(str, CP_UTF8) };
	}
};

class CDirectoryHelper
{
public:
	static bool IsDirectoryExists(const std::string& dirPath)
	{
		DWORD dwAttrib = ::GetFileAttributesA(dirPath.c_str());
		return (dwAttrib != INVALID_FILE_ATTRIBUTES &&
			(dwAttrib & FILE_ATTRIBUTE_DIRECTORY));
	}

	static bool CreateDirectory(const std::string& dirPath)
	{
		return ::CreateDirectoryA(dirPath.c_str(), NULL) != FALSE;
	}
};

class CAnsysHelper
{
public:
	static int AnsSendCommand(const char* ansCommand)
	{
		return ::cAnsSendCommand(const_cast<char*>(ansCommand));
	}

	static int AnsSendCommand(const std::string& ansCommand)
	{
		return AnsSendCommand(const_cast<char*>(ansCommand.c_str()));
	}

	static void RunAnsysCommand(const std::string& ansCommand)
	{
		AnsSendCommand("/uis,msgpop,3");
		auto ansCommands = CStringHelper::SplitString(ansCommand, ENDLINE);
		for (auto&& cmdLine : ansCommands)
		{
			AnsSendCommand(CStringHelper::Trim(cmdLine));
		}
		AnsSendCommand("/uis,msgpop,2");
	}

	static void RunAnsysCommand(const char* ansCommand)
	{
		RunAnsysCommand(std::string{ ansCommand });
	}

	static double GetDoubleVar(const char* varName)
	{
		double retValue = 0.0;
		char strMsg[MAX_PATH];
		int iType = 0;
		int res = cAnsEvalExpr(const_cast<char*>(varName), &retValue, strMsg, &iType);
		ATLASSERT(res == 0 && iType == 1);
		return retValue;
	}

	static int GetIntVar(const char* varName)
	{
		return static_cast<int>(GetDoubleVar(varName));
	}

	static std::string GetStringVar(const char* varName)
	{
		double retValue = 0.0;
		char strMsg[MAX_PATH];
		int iType = 0;
		int res = cAnsEvalExpr(const_cast<char*>(varName), &retValue, strMsg, &iType);
		ATLASSERT(res == 0 && iType == 2);

		std::string str{ strMsg };
		CStringHelper::ToLower(str);
		return std::move(str);
	}

	static std::vector<double> GetDoubleArray(const char* varName, int arrSize)
	{
		std::vector<double> arr;

		double* pDataBuf = new double[arrSize];
		ATLASSERT(pDataBuf);
		::cAnsGetVect(pDataBuf, const_cast<char*>(varName));
		arr.assign(pDataBuf, pDataBuf + arrSize);
		delete[] pDataBuf;
		pDataBuf = nullptr;

		return arr;
	}

	static std::vector<int> GetIntArray(const char* varName, int arrSize)
	{
		std::vector<int> arr;

		double* pDataBuf = new double[arrSize];
		ATLASSERT(pDataBuf);
		::cAnsGetVect(pDataBuf, const_cast<char*>(varName));
		arr.resize(arrSize);
		std::transform( pDataBuf, pDataBuf + arrSize, arr.begin(),
			[](double v) { return static_cast<int>(v); }	// convert float number to integer
		);
		delete[] pDataBuf;
		pDataBuf = nullptr;

		return arr;
	}

	static void DeleteOrdVar(const char* varName)
	{
		std::stringstream ss;
		ss << varName << " = " << ENDLINE;
		RunAnsysCommand(ss.str());
	}

	static void DeleteArrayVar(const char* varName)
	{
		std::stringstream ss;
		ss << "*del," << varName << ",,nopr" << ENDLINE;
		RunAnsysCommand(ss.str());
	}

	static void PrintString(const char* format, ...)
	{
		char buffer[BUFFER_SIZE];
		va_list args;
		va_start(args, format);
		::vsprintf_s(buffer, BUFFER_SIZE, format, args);
		va_end(args);

		::cAns_printf(buffer);
	}

	static void PrintDebugString(const char* format, ...)
	{
#ifdef _DEBUG
		char buffer[BUFFER_SIZE];
		va_list args;
		va_start(args, format);
		::vsprintf_s(buffer, BUFFER_SIZE, format, args);
		va_end(args);

		cAns_printf("\n********** Debug message start **********\n");
		cAns_printf(buffer);
		cAns_printf("\n********** Debug message end   **********\n");
#endif // _DEBUG
	}
};

enum class GroupType
{
	Node = 1,
	Element = 2
};

class CAnsysMeshModel
{
private:
	void SaveUserSelection() const
	{
		CAnsysHelper::RunAnsysCommand(R"(
			/prep7
			/uis,msgpop,3
			cm,cme_user_selected_elems_,elem
		)");
	}

	void RestoreUserSelection() const
	{
		CAnsysHelper::RunAnsysCommand(R"(
			cmdele,cmn_export_nodes_
			cmdele,cme_export_elems_
			cmsel,s,cme_user_selected_elems_	! restore user's initial selection
			cmdele,cme_user_selected_elems_
			/uis,msgpop,2
		)");
	}

	void GetInternalElemTypes(const std::vector<int>& exportedElemTypeNums)
	{
		std::stringstream ss;
		ss << R"(
			*get,etmax_,etyp,,num,max
			*dim,ets_,array,etmax_
			*set,etc_,0				! elem type count
			*do,i,1,etmax_
				*get,etnam_,etyp,i,attr,enam
				esel,s,ename,,etnam_
				*get,ec_,elem,,count
				*if,ec_,gt,0,then
					etc_ = etc_+1
					ets_(etc_) = etnam_
				*endif	
			*enddo
			etmax_ = 
		)";
		CAnsysHelper::RunAnsysCommand(ss.str());
		int elemTypeCount = CAnsysHelper::GetIntVar("etc_");
		auto elemTypeNums = CAnsysHelper::GetIntArray("ets_", elemTypeCount);
		for (size_t i = 0; i < elemTypeNums.size(); i++)
		{
			CAnsysHelper::PrintDebugString("elemTypes[%d] = %d", i, elemTypeNums[i]);
		}
		CAnsysHelper::DeleteArrayVar("ets_");
		CAnsysHelper::DeleteOrdVar("etc_");

		CAnsysHelper::PrintDebugString("elemTypeCount = %d", elemTypeCount);
		m_ElemTypeNums.assign(elemTypeNums.begin(),
			std::remove_if(elemTypeNums.begin(), elemTypeNums.end(),
				[&](int et) { return std::find(exportedElemTypeNums.cbegin(), exportedElemTypeNums.cend(), et) == exportedElemTypeNums.cend()
				|| this->InternalSupportedElemTypes.find(et) == this->InternalSupportedElemTypes.end(); }));

		ss.str("");
		for (size_t i = 0; i < m_ElemTypeNums.size(); i++)
		{
			ss << "esel," << (i == 0 ? "s" : "a") << ",ename,," << m_ElemTypeNums[i] << ENDLINE;
		}
		ss << R"(
			cmsel,r,cme_user_selected_elems_
			cm,cme_export_elems_,elem
			nsle,s,corner		! ****** only corner nodes are supported at present ******
			cm,cmn_export_nodes_,node
		)";
		CAnsysHelper::RunAnsysCommand(ss.str());
	}

	void GetInternalElemData()
	{
		for (auto&& etn : m_ElemTypeNums)	// for each element type
		{
			auto saet = InternalSupportedElemTypes.find(etn);
			assert(saet != InternalSupportedElemTypes.end());
			auto nodeCount = std::get<0>((*saet).second);	// node count of current type of element
			std::stringstream ss;
			ss << "cmsel,s,cme_export_elems_" << ENDLINE
				<< "esel,r,ename,," << etn << ENDLINE
				<< "*get,ec_,elem,,count" << ENDLINE
				<< "*if,ec_,gt,0,then" << ENDLINE
				<< "	*dim,elemData_,array,ec_," << (nodeCount + 1) << ENDLINE
				<< "	*get,em_,elem,,num,min" << ENDLINE
				<< "	*set,en_,em_" << ENDLINE
				<< "	*do,ii,1,ec_" << ENDLINE
				<< "		elemData_(ii,1) = en_" << ENDLINE;	// elem number	
			for (int i = 1; i <= nodeCount; i++)
			{
				ss << "		elemData_(ii," << (i+1) << ") = nelem(en_," << i << ")" << ENDLINE;
			}
			ss << "			en_ = elnext(en_)" << ENDLINE
				<< "	*enddo" << ENDLINE
				<< "	en_ =" << ENDLINE
				<< "	em_ =" << ENDLINE
				<< "*endif" << ENDLINE;
			CAnsysHelper::RunAnsysCommand(ss.str());

			auto elemCount = CAnsysHelper::GetIntVar("ec_");
			if (elemCount > 0)
			{
				std::vector<int> base(1 + nodeCount);
				for (size_t i = 0; i < base.size(); ++i)
				{
					base[i] = static_cast<int>(i*elemCount);
				}

				auto rawElemData = CAnsysHelper::GetIntArray("elemData_", elemCount*(1 + nodeCount));
				std::vector<std::vector<int>> elemData(elemCount);
				for (int i = 0; i < elemCount; i++)		// for each element
				{
					std::vector<int> ed(nodeCount + 1);
					for (int j = 0; j <= nodeCount; j++)
					{
						ed[j] = rawElemData[base[j] + i];
					}
					elemData[i] = std::move(ed);
				}
				m_ElemData[etn] = std::move(elemData);
			}

			CAnsysHelper::DeleteOrdVar("ec_");
			CAnsysHelper::DeleteArrayVar("elemData_");
		}
	}

	void GetInternalNodeData()
	{
		CAnsysHelper::RunAnsysCommand(R"(
			cmsel,s,cmn_export_nodes_
			*get,nc_,node,,count
			*dim,nd_,array,nc_,4
			*get,nm_,node,,num,min
			*set,nn_,nm_
			*do,ii,1,nc_
				nd_(ii,1) = nn_		! node number
				nd_(ii,2) = nx(nn_)	! X coordinate
				nd_(ii,3) = ny(nn_)	! Y coordinate
				nd_(ii,4) = nz(nn_)	! Z coordinate
				nn_ = ndnext(nn_)
			*enddo
			nn_ =
			nm_ =
		)");

		auto nodeCount = CAnsysHelper::GetIntVar("nc_");		// Get node count
		if (nodeCount > 0)
		{
			auto rawNodeData = CAnsysHelper::GetDoubleArray("nd_", nodeCount * 4);		// Get node data
			std::array<int, 3> base{ nodeCount, nodeCount * 2, nodeCount * 3 };
			for (int i = 0; i < nodeCount; i++)
			{
				int nodeNum = static_cast<int>(rawNodeData[i]);	// node number
				double x = rawNodeData[base[0] + i];
				double y = rawNodeData[base[1] + i];
				double z = rawNodeData[base[2] + i];
				m_NodeData[nodeNum] = std::make_tuple(x, y, z);
			}
		}

		CAnsysHelper::DeleteArrayVar("nd_");
		CAnsysHelper::DeleteOrdVar("nc_");
	}

	void GetInternalGroupData()
	{
		CAnsysHelper::RunAnsysCommand(R"(
			*get,compCountAll_,comp,0,ncomp
		)");
		auto groupCountAll = CAnsysHelper::GetIntVar("compCountAll_");
		CAnsysHelper::DeleteOrdVar("compCountAll_");
		CAnsysHelper::PrintDebugString("groupCountAll = %d", groupCountAll);

		std::stringstream ss;
		for (int i = 1; i <= groupCountAll; i++)
		{
			ss.str("");
			ss << "*get,compName_,comp," << i << ",name" << ENDLINE
				<< "*get,compType_,comp,compName_,type" << ENDLINE;
			CAnsysHelper::RunAnsysCommand(ss.str());
			auto groupName = CAnsysHelper::GetStringVar("compName_");
			CAnsysHelper::DeleteOrdVar("compName_");
			CAnsysHelper::PrintDebugString("groupName = %s", groupName.c_str());
			if (groupName[groupName.length()-1] == '_')		// skip the group with the name ending with '_'
			{
				continue;
			}

			auto gt = CAnsysHelper::GetIntVar("compType_");
			CAnsysHelper::DeleteOrdVar("compType_");
			CAnsysHelper::PrintDebugString("gt = %d", gt);
			if (!((static_cast<int>(GroupType::Node) <= gt) && (gt <= static_cast<int>(GroupType::Element))))		// only node and element group can be exported
			{
				continue;
			}

			auto groupType = static_cast<GroupType>(gt);
			static std::map<GroupType, std::tuple<std::string, std::string, std::string>> groupMap
			{
				std::make_pair(GroupType::Node,    std::make_tuple("cmn_export_nodes_", "node", "ndnext")),		// (groupType, (componetName, groupTypeLabel, nextFunctionName))
				std::make_pair(GroupType::Element, std::make_tuple("cme_export_elems_", "elem", "elnext"))
			};

			auto gm = groupMap[groupType];
			ss.str("");
			ss << "cmsel,s," << groupName << ENDLINE
				<< "cmsel,r," << std::get<0>(gm) << ENDLINE
				<< "*get,count_," << std::get<1>(gm) << ",,count" << ENDLINE
				<< "*dim,data_,array,count_" << ENDLINE
				<< "*get,num_," << std::get<1>(gm) << ",,num,min" << ENDLINE
				<< "*do,jj,1,count_" << ENDLINE
				<< "	data_(jj) = num_" << ENDLINE
				<< "	num_ = " << std::get<2>(gm) << "(num_)" << ENDLINE
				<< "*enddo" << ENDLINE;
			CAnsysHelper::RunAnsysCommand(ss.str());
			//CAnsysHelper::PrintDebugString("groupDataCommand: \n%s", ss.str().c_str());
			auto count = CAnsysHelper::GetIntVar("count_");
			if (count > 0)
			{
				auto data = CAnsysHelper::GetIntArray("data_", count);
				m_GroupData[groupName] = std::make_tuple(groupType, std::move(data));
			}
			CAnsysHelper::DeleteOrdVar("count_");
			CAnsysHelper::DeleteArrayVar("data_");
		}
	}

public:
	void RetriveAnsysInternalMeshData(const std::vector<int>& exportedElemTypeNums)
	{
		SaveUserSelection();

		GetInternalElemTypes(exportedElemTypeNums);
		GetInternalElemData();
		GetInternalNodeData();
		GetInternalGroupData();

		RestoreUserSelection();
	}

	std::map<int, std::tuple<double, double, double>>& GetNodeData() const { return m_NodeData; }
	std::map<int, std::vector<std::vector<int>>>& GetElemData() const { return m_ElemData; }
	std::map<std::string, std::tuple<GroupType, std::vector<int>>>& GetGroupData() const { return m_GroupData; }

public:
	//
	// ansysElemType : (typeNum, (nodeCount, typeName))
	//
	std::map<int, std::tuple<int, std::string>> InternalSupportedElemTypes
	{
		// 2D elements
		std::make_pair( 42, std::make_tuple(4, std::string{ "Plane42" })),
		std::make_pair(182, std::make_tuple(4, std::string{ "Plane182" })),
		std::make_pair(  3, std::make_tuple(2, std::string{ "Beam3" })),
		std::make_pair(  1, std::make_tuple(2, std::string{ "Link1" })),

		// 3D elements
		std::make_pair( 45, std::make_tuple(8, std::string{ "Solid45" })),
		std::make_pair( 43, std::make_tuple(4, std::string{ "Shell43" })),
		std::make_pair( 41, std::make_tuple(4, std::string{ "Shell41" })),
		std::make_pair(  4, std::make_tuple(2, std::string{ "Beam4" })),
		std::make_pair(  8, std::make_tuple(2, std::string{ "Link8" })),
	};

private:
	std::vector<int> m_ElemTypeNums;
	std::map<int, std::tuple<double, double, double>> m_NodeData;					// nodeData : (nodeNum, (x, y, z))
	std::map<int, std::vector<std::vector<int>>> m_ElemData;						// elemData : (elemType, (elemNum, n1, n2, n3, n4, ...))
	std::map<std::string, std::tuple<GroupType, std::vector<int>>> m_GroupData;		// groupData: (groupName, (groupType, groupElems))
};

